
import { trackError, trackVueError, recordFunc } from './modules/trackError'
import { trackClick } from './modules/trackEvent'
import { trackPageStay } from './modules/trackPageStay'
import { trackRequest } from './modules/trackRequest'
import { trackPerformance } from './modules/trackPerformance'
import { zlibSync, strToU8, strFromU8 } from 'fflate'

class Track {
    // 方法
    // 初始化数据
    init(options, isFunction) {
        if (options && Object.prototype.toString.call(options) === '[object Object]' && Object.keys(options).length) {
            this.options = options
            trackClick.apply(this)
            trackPageStay.apply(this)
            trackRequest.apply(this)
            trackPerformance.apply(this)
            trackError.apply(this)
            recordFunc.apply(this)
            isFunction ? isFunction() : ''
            this.consoleLog('track初始化完成')
        }else{
            this.consoleLog('请添加初始化需要的配置')
        }
    }
    // Vue和React内部都对错误做了拦截处理的,所以只能特地去兼容他们
    // 专门给Vue初始化插件用的方法,Vue2和Vue3框架使用.use会自动读取对象里的install方法,install方法返回的第一个形参都是Vue实例化对象
    install(Vue, options) {
        if (!Vue || !Vue.config) return
        // Vue框架初始化插件完成,执行完监听它的错误方法api
        this.init(options, () => {
            trackVueError(Vue, this)
        })
    }
    // 判断插件里要不要日志
    consoleLog() {
        if (this.options.needLog) {
            console.log(...arguments)
        }
    }
    // 判断要不要某种埋点类型
    needTrack(type) {
        const { chain } = this.options
        const chainOtherArr = []
        for (let i = 0; i < chain.length; i++) {
            const item = chain[i]
            // 判断对象里有没pointList属性先,没有就给它组装成有pointList属性的对象,这样两种对象数据格式就统一了
            if (!item.pointList) {
                item.pointList = [{ ...item }]
                item.status = { beginTime: 0 }
                delete item.handler
                delete item.rule
                delete item.type
            }
            item.id = i + 1
            const otherArr = item.pointList.filter((itemB, index) => {
                itemB.parentId = item.id
                itemB.id = Number(`${item.id}${index + 1}`)
                itemB.executes = []  // 正在执行中的
                return itemB.type === type
            })
            chainOtherArr.push(...otherArr)
        }
        return chainOtherArr
    }
    // 根据id获取到父
    getParent(parentId) {
        const { chain } = this.options
        const otherArr = chain.filter(item => item.id === parentId)
        return otherArr[0]
    }
    // 整理上报数据和触发指定对象的函数
    sendDataBefore(needTracks, type, dataObj) {
        this.sending = true
        if(['error','navigationTime','resourceTime'].includes(type)){
            // 如果是error类型,进行错误去重,后端那边后边也要做好错误去重,毕竟前端想在调接口前处理好去重,目前只能存到一个临时变量里
            if(type === 'error'){
                const errorStr = JSON.stringify(dataObj)
                if (~this.allErrors.indexOf(errorStr)) {
                    this.consoleLog('该错误刚才已经上报过了')
                    return
                } else {
                    this.allErrors.push(errorStr)
                    this.options.openErrorRecord ? dataObj.events = this.utoa(JSON.stringify(this.events)) : ''
                }
            }
            const sendData = this.getSendData(type, dataObj)
            this.options.send(sendData)
        }else{
            // 上报数据,链路对象就要绑定一次this的指向
            for (const endItem of needTracks) {
                const sendData = this.getSendData(type, dataObj)
                const needParent = this.getParent(endItem.parentId)
                // 看有没回调函数handler,如果没有回调函数handler,就直接触发上报方法就可以了
                if (endItem.handler && Object.prototype.toString.call(endItem.handler) === '[object Function]') {
                    endItem.handler.apply(needParent, [sendData, this.options.send])
                } else {
                    this.options.send(sendData)
                }
            }
        }
        this.sending = false
    }
    // 整理要上报的数据
    getSendData(type, val) {
        val.type = type
        !val.currentTime ? val.currentTime = new Date().getTime() : ''  // 当前时间
        !val.pageTitle ? val.pageTitle = document.title : ''  // 页面标题
        !val.pageUrl ? val.pageUrl = location.href : ''  // 页面url
        return val
    }
    // 判断有没有内容完全一致的在executes数组里,有就返回true,没有就返回false,同事追加到executes数组里
    isInExecutes(myItem, content) {
        const { executes } = myItem
        const myObj = { content, lastTime: null }
        if (!executes.length) {
            executes.push(myObj)
            return myObj
        } else {
            for (const item of executes) {
                // 有一个符号条件的就说明之前已经点了
                if (item.content === content) {
                    return item
                }
            }
            executes.push(myObj)
            return myObj
        }
    }
    // 防抖,语法使用者没有做防抖造成高频率的上报,200毫秒内触上报同一个事件..正常情况下是不可能的,只会是乱点
    // 这是不是同一个还要具体问题具体问题
    // 点击事件就要判断页面url和dom是不是同一个,请求就是页面和所有请求参数是不是同一个
    antiShake(insideObj, callback) {
        insideObj.lastTime ? clearTimeout(insideObj.lastTime) : ''
        insideObj.lastTime = setTimeout(() => {
            callback()
        }, 200)
    }
    // 根据content属性删除掉不要的对象
    deleteExecutesItem(item, content) {
        const index = item.executes.findIndex((item) => item.content === content)
        item.executes.splice(index, 1)
    }
    // 压缩
    utoa(data) {
        // 将字符串转成Uint8Array
        const buffer = strToU8(data)
        // 以最大的压缩级别进行压缩，返回的zipped也是一个Uint8Array
        const zipped = zlibSync(buffer, { level: 9 })
        // 将Uint8Array重新转换成二进制字符串
        const binary = strFromU8(zipped, true)
        // 转成Blob对象方便传给后端保存
        const blob = new Blob([binary], { type: "application/zip" })
        return blob
    }
}

export default new Track()
